<?php
/**
 * Messenger.php
 * 
 * @category   Naneau
 * @package    Naneau_MSN
 * @copyright  Copyright (c) 2007 Maurice Fonk - http://naneau.nl
 * @version    0.1
 */

/**
 * http client for passport login
 */
require_once 'Zend/Http/Client.php';

/**
 * contact list
 */
require_once 'Naneau/MSN/Messenger/ContactList.php';

/**
 * single contact
 */
require_once 'Naneau/MSN/Messenger/Contact.php';

/**
 * Naneau_MSN_Messenger
 * 
 * a simple class for connecting to the MSN messenger network
 *
 * @category   Naneau
 * @package    Naneau_MSN
 * @copyright  Copyright (c) 2007 Maurice Fonk - http://naneau.nl
 */
class Naneau_MSN_Messenger {

    /**
     * online status
     * @var string
     */
    const ONLINE = 'NLN';

    /**
     * debug?
     *
     * @var bool
     */
    private $_debug = false;

    /**
     * connection
     *
     * @var resource
     */
    private $_con;

    /**
     * the number of commands written to the server
     *
     * @var int
     */
    private $_msgCount = 1;

    /**
     * do we have a connection to a server?
     *
     * @var bool
     */
    private $_connected = false;

    /**
     * received data (information about user, etc.)
     *
     * @var array
     */
    private $_data = array();

    /**
     * the contact list
     *
     * @var Naneau_MSN_ContactList
     */
    private $_contactList;

    /**
     * server to connect to
     *
     * @var string
     */
    protected $server = 'messenger.hotmail.com';

    /**
     * port used for that connection
     *
     * @var int
     */
    protected $port = 1863;

    /**
     * passport nexus url
     *
     * @var string
     */
    protected $passport = 'https://nexus.passport.com/rdr/pprdr.asp';

    /**
     * constructor, pass it your email and password
     * 
     * you must have a passport login registered for the combination 8^]
     *
     * @param string $email
     * @param string $password
     * @return Naneau_MSN_Messenger
     */
    public function __construct($email, $password, $autoConnect = true) {
        $this->email = $email;
        $this->password = $password;
        //assign

        if ($autoConnect) {
            $this->_connected = $this->connect($email, $password, $this->server, $this->port);
            //try to connect

        }
    }

    /**
     * check whether the login attempt was succesful
     *
     * @return bool
     */
    public function loggedIn() {
        return $this->_connected;
    }

    /**
     * get the contact list
     * 
     * @return Naneau_MSN_Messenger_ContactList
     */
    public function getContactList() {
        if (!$this->_connected) {
            throw new Exception('Not connected, can\'t retrieve contact list');
        }

        if ($this->_contactList == null) {
            $this->buildContactList();
        }

        return $this->_contactList;
        //new contact list
    }

    /**
     * set status
     * 
     * @param int $status
     * @return void
     */
    public function setStatus($status) {
        if ($this->_contactList == null) {
            $this->getContactList();
        }
        //contact list must have been retrieved

        $this->sendCommand('CHG', $status);
    }

    /**
     * connect to the server
     *
     * @return bool
     * @throws Exception if it can't open a connection
     */
    private function connect() {
        $this->getSock($this->server, $this->port);
        //create connection

        $this->sendCommand('VER', 'MSNP9 CVR0');
        //start login

        while ($response = $this->getResponse()) {
            //login chain

            switch (substr($response, 0, 3)) {
                case 'USR':
                    //user related command
                    $data = explode(' ', trim($response));
                    //split the data
                    if ($data[2] == 'TWN') {
                        //challenge (not logged in yet)
                        if ($ticket = $this->passportLogin($data[4], $this->email, $this->password)) {
                            //try to do 'real' login with passport
                            $this->sendCommand('USR', 'TWN S ' . $ticket);
                            //replay with ticket
                        }
                        else {
                            //passport login failed
                            return false;
                        }
                    }
                    else {
                        //succesful login!
                        $this->fetchData();
                        //get data
                        return true;
                    }

                    break;
                case 'VER':
                    //server returns version
                    $this->sendCommand('CVR', '0x0409 win 4.10 i386 MSNMSGR 7.0.0816 MSMSGS ' . $this->email);
                    //reply with our 'version' and email
                    break;
                case 'CVR':
                    //protocol information
                    $this->sendCommand('USR', 'TWN I ' . $this->email);
                    //try to login
                    break;
                case 'XFR':
                    //redirect
                    $server = explode(' ', $response);
                    $server = explode(':', $server[3]);
                    //split into server and port
                    $this->getSock($server[0], intval($server[1]));
                    //reconnect
                    $this->sendCommand('VER', 'MSNP9 CVR0');
                    //restart with the version
                    break;
                default:
                    //unknown command
                    //we can't have those!
                    //really, we can't :x
                    return false;
                    break;
            }
        }
    }

    /**
     * receive contact list info
     *
     */
    private function buildContactList() {
        $contacts = array();
        //the array of contacts

        $this->sendCommand('SYN', 0);
        //start synchronisation of contact list (pretend it's never happend)

        while ($response = $this->getResponse()) {
            if (substr($response, 0, 3) == 'LST') {
                $data = explode(' ', $response);
                $contacts[] = new Naneau_MSN_Messenger_Contact($data[1], urldecode($data[2]));
            }
        }

        $this->_contactList = new Naneau_MSN_Messenger_ContactList($contacts);
    }

    /**
     * fetch extra data after login
     * 
     * @return void
     */
    private function fetchData() {
        while ($response = $this->getResponse()) {
            $data = explode(':', $response);
            if (isset($data[1])) {
                $this->_data[$data[0]] = trim($data[1]);
                //save data
            }
        }
    }

    /**
     * create a socket
     *
     * @param string $server
     * @param int $port
     */
    private function getSock($server, $port) {
        $this->_msgCount = 1;
        //reset msgCount

        if ($this->_con) {
            fclose($this->_con);
        }
        //if connection, reset it

        if (!($this->_con = @fsockopen($server, $port, $errno, $errstr, 2))) {
            //can't connect
            throw new Exception('Can\'t open connection socket: ' . $server . ':' . $port);
        }
    }

    /**
     * do the passport login
     *
     * @param string $server
     * @param int $port
     */
    private function passportLogin($challenge) {
        $http = new Zend_Http_Client($this->passport);
        $response = $http->request();
        //do request to passport
        $headers = $response->getHeaders();
        //get response headers

        preg_match ('/DALogin=(.*?),/', $headers['Passporturls'], $matches);
        if (isset($matches[1])) {
            //if the first parenthesized part (the real url) exists

            $url = $matches[1];
            //the login url

            $http = new Zend_Http_Client('https://' . $matches[1]);
            $http->setHeaders('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom,sign-in=' . $this->email . ',pwd=' . $this->password . ',' . $challenge . '"');
            $response = $http->request();
            $headers = $response->getHeaders();
            //send login request to 'real' server we received in first request

            preg_match ('/from-PP=\'(.*?)\'/', $headers['Authentication-info'], $matches);
            //try to get a ticket out of the response headers

            return isset($matches[1]) ? $matches[1] : false;
            //must get a ticket, or it failed
        }
        else {
            //something went wrong, no url in the response
            return false;
        }

    }

    /**
     * get a response from the server, or false if there is none
     *
     * @return string|bool $response
     */
    public function getResponse() {
        if (!feof($this->_con) && $response = @fgets($this->_con)) {
            //not end of file for connection, and there is a response

            if ($this->_debug) {
                echo '<<< ' . $response . '<br />';
            }
            return $response;
        }
        return false;
    }

    /**
     * write a command the socket
     *
     * @param string $command
     * @param string $options
     * @return void
     */
    public function sendCommand($command, $options) {
        $dString = $command . ' ' . $this->_msgCount++  . ' ' . $options . "\r\n";
        //the command

        if ($this->_debug) {
            echo '>>> ' . $dString . '<br />';
        }

        fwrite($this->_con, $dString);
        //output it
    }
}